﻿
namespace Patterns4Net.PatternEnforcer.Core
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Mono.Cecil;
    using Patterns4Net.Core;
    using Patterns4Net.Core.CecilExtensions;
    using Patterns;
    using Attributes;

    /// <summary>
    /// Provides a Facade to Pattern Enforcer.
    /// </summary>
    [Export()]
    public class PatternEnforcer
    {
        private readonly PatternsLocator patternsLocator;

        private readonly IPatternCheckersLocator[] patternCheckersLocators;
        
        [ImportingConstructor]
        public PatternEnforcer(
            PatternsLocator patternsLocator, 
            [ImportMany]IPatternCheckersLocator[] patternCheckersLocators)
        {
            Contract.Requires(patternsLocator != null);
            Contract.Requires(patternCheckersLocators != null);

            this.patternsLocator = patternsLocator;
            this.patternCheckersLocators = patternCheckersLocators;
        }

        /// <summary>
        /// Checks correct implementations of annotated design patterns in given assembly.
        /// </summary>
        public IEnumerable<ResultItem> CheckAssembly(AssemblyDefinition assembly)
        {
            var toBeChecked = this.patternsLocator.GetPatternsFromAssembly(assembly);
            var result = new List<ResultItem>();

            this.CheckTypes(toBeChecked, result);
            this.CheckMethods(toBeChecked, result);
            return result;            
        }

        /// <summary>
        /// Checks correct implementations of annotated design patterns in given assembly.
        /// </summary>
        public IEnumerable<ResultItem> CheckAssembly(string filename)
        {
            AssemblyDefinition assemblyDefinition = null;
            try
            {
                assemblyDefinition = AssemblyResolver.Instance.ReadAssembly(filename);
            }
            catch (Exception ex)
            {
                throw new PatternEnforcerException(
                    string.Format(
                        "Assembly {0} cannot be loaded. An exception {1} occurred. Exception message: {2}", 
                        filename, 
                        ex.GetType().Name, 
                        ex.Message)
                    , ex);
            }

            return this.CheckAssembly(assemblyDefinition);
        }

        /// <summary>
        /// Returns <see cref="CheckerResult"/> of given pattern. 
        /// If a checker for given pattern does not exist, returns <c>null</c>.
        /// </summary>
        /// <param name="pattern">Pattern instance to be checked.</param>
        /// <returns>The result of the verification, or null.</returns>
        public CheckerResult CheckPattern(IPattern pattern)
        {
            var patternChecker = this.GetCheckerFor(pattern);
            if (patternChecker == null)
            {
                return null;
            }

            return patternChecker.Check(pattern);
        }

        private void CheckMethods(PatternsLocationResult toBeChecked, IList<ResultItem> result)
        {
            foreach (var methodAndPatterns in toBeChecked.Methods)
            {
                if (IgnoreCheck(methodAndPatterns.MethodDefinition))
                {
                    continue;
                }

                this.CheckMethod(methodAndPatterns.MethodDefinition, methodAndPatterns.Patterns, result);
            }
        }

        private void CheckMethod(MethodDefinition method, IEnumerable<IPattern> patterns, IList<ResultItem> result)
        {
            var methodResults = new Dictionary<IPattern, CheckerResult>();
            foreach (var pattern in patterns)
            {
                var patternChecker = this.GetCheckerFor(pattern);
                if (patternChecker == null)
                {
                    continue;
                }

                var checkResult = DoCheck(patternChecker, pattern);
                methodResults.Add(pattern, checkResult);
            }

            result.Add(new ResultItem(method, methodResults));
        }

        private void CheckTypes(PatternsLocationResult toBeChecked, IList<ResultItem> result)
        {
            foreach (var typeAndPatterns in toBeChecked.Types)
            {
                if (IgnoreCheck(typeAndPatterns.TypeDefinition))
                {
                    continue;
                }

                this.CheckType(typeAndPatterns.TypeDefinition, typeAndPatterns.Patterns, result);
            }
        }

        private void CheckType(TypeDefinition type, IEnumerable<IPattern> patterns, IList<ResultItem> result)
        {
            var typeResult = new Dictionary<IPattern, CheckerResult>();
            foreach (var pattern in patterns)
            {
                var patternChecker = this.GetCheckerFor(pattern);
                if (patternChecker == null)
                {
                    continue;
                }

                var checkResult = DoCheck(patternChecker, pattern);
                typeResult.Add(pattern, checkResult);
            }

            result.Add(new ResultItem(type, typeResult));
        }

        private IPatternChecker GetCheckerFor(IPattern pattern)
        {
            foreach (var checkersLocator in this.patternCheckersLocators.Reverse())
            {
                var result = checkersLocator.GetCheckerFor(pattern);
                if (result != null)
                {
                    return result;
                }
            }

            return null;
        }

        private static CheckerResult DoCheck(IPatternChecker checker, IPattern pattern)
        {
            try
            {
                return checker.Check(pattern);
            }
            catch (Exception ex)
            {
                throw new PatternEnforcerException(
                    string.Format(
                        "Pattern checker {0} has thrown an exception of type {1} when checking the {2} pattern. Exception message: {3}",
                        checker.GetType().Name,
                        ex.GetType().Name,
                        pattern.Name,
                        ex.Message),
                    ex);
            }
        }

        private static bool IgnoreCheck(IMemberDefinition member)
        {
            return member.CustomAttributes.Any(attr => attr.AttributeType.IsEqual(typeof(PatternEnforcerIgnoreAttribute)));
        }

        /// <summary>
        /// Represents an item of Pattern Enforcer check result.
        /// </summary>
        public class ResultItem
        {
            public ResultItem(MemberReference member, IDictionary<IPattern, CheckerResult> checkerResults)
            {
                this.Member = member;
                this.CheckerResults = checkerResults;
            }

            public MemberReference Member { get; private set; }

            public IDictionary<IPattern, CheckerResult> CheckerResults { get; private set; }

            public bool IsValid
            {
                get
                {
                    Contract.Ensures(Contract.Result<bool>() != this.HasErrors);
                    return this.CheckerResults.All(x => x.Value.IsValid);
                }
            }

            public bool HasErrors
            {
                get
                {
                    Contract.Ensures(Contract.Result<bool>() != this.IsValid);
                    return this.IsValid == false;
                }
            }
        }
    }

    /// <summary>
    /// Represents an exception that can be thrown be <see cref="PatternEnforcer"/> facade class.
    /// </summary>
    public class PatternEnforcerException : ApplicationException
    {
        public PatternEnforcerException(string message, Exception innerException = null) 
            : base(message, innerException)
        {
        }
    }
}
